#!/usr/bin/env ruby

require 'yaml'
require 'fileutils'
require 'digest'

# used to do package for user side
# pack_source
#   git_last_tag
#     - last repo tag for the repo
#   get_modified_files
#     - get modifiled files since the last
#   get_new_files
#     - get untracked files
#   should_update
#     - check if there is any change since last pack
#     - if no change, skip and use the on packaged last time
#     - if changed, do new package
class PackChange
  attr_reader :dest_cgz

  def initialize(repo_dir, do_pack = true)
    @repo_dir = repo_dir
    @do_pack = do_pack
    @repo_name = File.basename(@repo_dir)
    @base_home = if ENV['LKP_ALIAS'] && ENV['LKP_CATEGORY']
                    "#{ENV['HOME']}/.cache/#{ENV['LKP_CATEGORY']}/#{ENV['LKP_ALIAS']}/lkp"
                  else
                    "#{ENV['HOME']}/.cache/lkp"
                  end
    @dest_test_home = if @repo_name == 'lkp-tests'
                        "#{@base_home}/LKP_SRC"
                      else
                        "#{@base_home}/LKP_SRC2"
                      end
    @dest_cgz = "#{@dest_test_home}/#{@repo_name}.cgz"
    @pass_file_type = %w[.md .bk .swp .zip .bak .yml]
  end

  def filter_files(files)
    files.delete_if do |file|
      if @repo_name == 'lkp-tests'
        next true if file.eql?('.swp')
        next true if file.start_with?('doc/')
        next true if file.start_with?('jobs/')
        next true if file.start_with?('spec/')
        next true if file.start_with?('repo/')
        next true if file.start_with?('hosts/')
        next true if file.start_with?('tmp/')
        next true if file.start_with?('workflows/')
        next true if file.start_with?('bin/gzip-')
        next true if file.start_with?('distro/') && !File::executable?(file) && !file.start_with?('distro/ks/')
        next true if file.start_with?('rootfs/addon/root/.ssh/')
      end

      # ignore top level files, they are often scratch files
      next true unless file.include? '/'

      # ignored file extension: '.md', '.bk', '.swp', '.zip', '.bak', '.yml'
      file_extension = File.extname(file)
      next true if @pass_file_type.include? file_extension

      false
    end
  end

  def get_modified_files(lkp_tag)
    %x(git -C #{@repo_dir} diff --diff-filter=ACMRTU  --name-only #{lkp_tag} 2>/dev/null).split("\n")
  end

  def get_new_files
    %x(git -C #{@repo_dir} status -u --short 2>/dev/null | grep -v "^ *[MD]"| awk '{print $2}').split("\n")
  end

  def should_update
    if File.exist? @dest_cgz
      files = %x(cd #{@repo_dir} && find */ -mindepth 1 -type f -newer #{@dest_cgz}).split("\n")
      filter_files(files)
      files.size > 0
    else
      true
    end
  end

  def pack_source
    tag = git_last_tag(@repo_dir)

    old_md5 = false
    old_md5 = create_lkp_delta_cpio(tag) if @do_pack and should_update
    md5 = Digest::MD5.hexdigest File.read(@dest_cgz)

    pkg_data = {
          'tag' => tag,
          'md5' => md5,
    }
      pkg_data['content'] = [File.read(@dest_cgz)].pack('m').chomp
    if old_md5 and old_md5 != md5
      puts "Will upload LKP source delta: #{@dest_cgz}"
    end
    pkg_data
  end

  def get_delta_files(tag)
    files = get_modified_files(tag).concat(get_new_files())
    filter_files(files)

    src_files = files.map do |file| 'lkp/lkp/src/' + file end

    # get all missing parent dirs
    cpio_dirs = []
    src_files.each do |file|
      dir = File.dirname(file)
      next if src_files.include?(dir)
      next if cpio_dirs.include?(dir)
      loop do
        cpio_dirs << dir
        dir = File.dirname(dir)
        break if cpio_dirs.include? dir
      end
    end

    files = cpio_dirs.concat(src_files).sort
    loop do
      break if files.empty?
      break if files[0].size > 12 # remove 'lkp/lkp/src' and its parents
      files.shift
    end
    files
  end

  def create_lkp_delta_cpio(tag)
    raise 'ERROR: No tag found. Please update your repo and then try again.' if tag.empty?

    lkp_files = get_delta_files(tag)
    return if lkp_files.empty?
    addon_files = lkp_files.select {|f| f.start_with? 'lkp/lkp/src/rootfs/addon/' }.map {|f| f.sub('lkp/lkp/src/rootfs/addon/', '')}

    lkp_dirs = ['lkp', 'lkp/lkp', 'lkp/lkp/src']

    tmp_out = "#{@dest_cgz}.tmp.#{$$}.#{rand(1_000_000)}"
    old_md5 = %x(
      set -e
      mkdir -p "#{@dest_test_home}"
      tmpdir=$(mktemp -d -p "#{@dest_test_home}" lkp_pack.XXXXXXXX)
      trap 'rm -rf "$tmpdir"' EXIT INT TERM
      cd "$tmpdir"

      mkdir -p lkp/lkp/src
      touch -d @0 #{lkp_dirs.join(' ')}

      cpio_file="$tmpdir/pkg.cpio"
      echo "#{lkp_dirs.join "\n"}" | cpio --quiet --reproducible -o --owner root:root -H newc -F "$cpio_file"

      rmdir lkp/lkp/src
      ln -s #{@repo_dir} lkp/lkp/src
      echo "#{lkp_files.join "\n"}" | cpio --quiet --reproducible -o --owner root:root -H newc -F "$cpio_file" --append

      #{addon_files.empty?} || {
        cd lkp/lkp/src/rootfs/addon
        echo "#{addon_files.join "\n"}" | cpio --quiet --reproducible -o --owner root:root -H newc -F "$cpio_file" --append
      }

      gzip -n -9 "$cpio_file"
      # If an existing artifact exists, output its md5 for caller
      [ -f "#{@dest_cgz}" ] && md5sum "#{@dest_cgz}"

      # Publish atomically: move built file to same dir with a temp name, then replace final
      mv -f "$cpio_file.gz" "#{tmp_out}"
      mv -f "#{tmp_out}" "#{@dest_cgz}"
    ).split.first
  end
end

def add_pkg_data(job_hash, do_pack)
    # do local pack
    # modified files since the last tag
    # untracked files
    pkg_data = {}

    unless job_hash.key?('pkg_data')
      lkp_repos = [ENV['LKP_SRC']]
      lkp_repos.insert(-1, ENV['LKP_SRC2']) if ENV['LKP_SRC2']

      lkp_repos.each do |repo|
        repo_name = File.basename(repo)
        do_package = PackChange.new(repo, do_pack)

        pkg_data[repo_name] = do_package.pack_source
      end
    end

    # init scheduler client
    # add pkg_data to job_hash
    job_hash['pkg_data'] = pkg_data unless pkg_data.empty?
end

def git_last_tag(repo_dir)
  tag = %x(git -C #{repo_dir} describe --abbrev=0 --tags).chomp
end

def pack_lkp_delta(base_commit)
  do_package = PackChange.new(ENV['LKP_SRC'], true)
  old_md5 = do_package.create_lkp_delta_cpio(base_commit)
  return unless old_md5
  do_package.dest_cgz
end
